﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics.Contracts;
using System.Data.Entity.Core.Metadata.Edm;

namespace DynamicFiltering
{

    /// <summary>
    /// Generates the code for a class that contains a single method that will return an IQueryable.
    /// Accepts list of EntityModelNavigators
    /// </summary>
    public class LinqGenerator
    {

        /// Entity namespace added dynamically - see MethodPre()
        private readonly static List<string> _usings = new List<string>() { "using System;", "using System.Linq;", "using System.Collections.Generic;" };

        public const string CLASS_NAME = "DaClass";
        public const string METHOD_NAME = "DaMethod";
        public const string NAMESPACE_NAME = "DaNamespace";

        private const string FROM = "from";
        private const string JOIN = "join";
        private const string IN = "in";
        private const string ON = "on";
        private const string CONTEXT = "context";
        private const string EQUALS = "equals";
        private const string SELECT = "select";
        private const string WHERE = "where";
        private const char STARTING_ALIAS = 'A';
        private const int STARTING_ALIAS_INDEX = 1;
        private const string QUERY_PARAM_NAME = "result";
        private const string LIST = "list";
        private const string LIST_TYPE = "List<int>";
        private const string NULLABLE = "({0} ?? 0)";

        private List<EntityModelNavigator> _entityModelNavigators;
        private List<string> _conditions;
        private string _queryResultEntityTypeName;
        private bool _finalTableIsChild;
        private int _listCount = 0;
        private bool _currentNavItemIsAssignmentTable;
        private readonly List<Tuple<string, NavigationItem>> _aliases = new List<Tuple<string, NavigationItem>>();

        /// <summary>
        /// Column name, nullable
        /// </summary>
        private Tuple<string, bool> _previousEntityKeyColumn = new Tuple<string, bool>( string.Empty, false );
        private Tuple<string, bool> _currentEntityKeyColumn = new Tuple<string, bool>( string.Empty, false );

        /// Example result of Generate:
        //using System;
        //using System.Linq;
        //using System.Collections.Generic;
        //using YourCompany.YourSystem.YourDataModel;
        //namespace DaNamespace
        //{
        //public static class DaClass
        //{
        //public static IQueryable<YourEntity> DaMethod( OSDSEntities context , List<int> list1, List<int> list2)
        //{
        //var result = from a1 in context.YourEntities
        //join a2 in context.EntityParents on a1.EntityParentId equals a2.Id
        //join b2 in context.OtherParents on a1.OtherParentId equals b2.Id
        //join c2 in context.EntityBridges on a1.EntityBridgeId equals c2.Id
        //where (a2.Id == 88888) && ( list1.Contains(b2.Id) ) && ( list2.Contains(c2.Id) )
        //select a1;
        //return result;
        //}
        //}
        //}
        public string Generate<ContextType>( List<EntityModelNavigator> emns, ContextType context )
        {
            _entityModelNavigators = emns;
            Contract.Requires( _entityModelNavigators.Count > 0 );
            _queryResultEntityTypeName = emns[0].Chain.First.Value.FromEntity;
            _entityModelNavigators.ForEach( emn =>
            {
                Contract.Requires( emn.Chain != null );
                /// The starting entity of each chain must be the same (overall entity to return is same)
                Contract.Requires( emn.Chain.First.Value.FromEntity == _queryResultEntityTypeName );
            } );

            _finalTableIsChild = false;
            _conditions = new List<string>( emns.Count );

            return string.Format( "{0}{1}{2}{3}{4}{5}",
                                    MethodPre<ContextType>( context ),
                                    FirstEntity(),
                                    Joins(),
                                    Conditions(),
                                    FinalSelect(),
                                    MethodPost() );
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>Code up to the method open</returns>
        private string MethodPre<ContextType>( ContextType context )
        {
            var result = string.Empty;

            string contextNamespace = string.Format( "using {1};{0}", Environment.NewLine, typeof( ContextType ).Namespace );
            if ( !_usings.Contains( contextNamespace ) )
            {
                _usings.Add( contextNamespace );
            }
            _usings.ForEach( u => result += string.Format( "{0}{1}", Environment.NewLine, u ) );

            result += string.Format( "{0}namespace {1}{0}{2}", Environment.NewLine, NAMESPACE_NAME, "{" );
            result += string.Format( "{0}public static class {2}{0}{1}", Environment.NewLine, "{", CLASS_NAME );
            result += string.Format( "{0}public static IQueryable<{1}> {2}( {3} {4} {5}){0}{6}",
                                    Environment.NewLine,
                                    _queryResultEntityTypeName,
                                    METHOD_NAME,
                                    context.GetType().Name,
                                    CONTEXT,
                                    AddListParameters( _entityModelNavigators ),
                                    "{" );
            return result;
        }

        /// <summary>
        /// Add in list parameters (if any exist) to main method signature
        /// Ex: ForecastEntities context, List<int> list1, List<int> list2
        /// </summary>
        /// <param name="emns"></param>
        /// <returns>like ", List<int> list1, List<int> list2"</returns>
        private string AddListParameters( List<EntityModelNavigator> emns )
        {
            var result = string.Empty;
            emns.ForEach( emn =>
            {
                if ( emn.ToEntityFilter is List<int> )
                {
                    /// We're assuming that the first param is context (see example above)
                    result += ", ";
                    result = string.Format( "{0}{1} {2}{3}", result, LIST_TYPE, LIST, ++_listCount );
                }
            } );

            _listCount = 0; // will need to use this list count again in AddListCondition()

            return result;
        }

        private string FirstEntity()
        {
            /// Each entity that is the first link in each chain has already been enforced to be the same; the entity set name will be the same as well.
            var firstEntityNavItem = _entityModelNavigators[0].Chain.First.Value;
            var firstEntityAlias = string.Format( "{0}{1}", STARTING_ALIAS, STARTING_ALIAS_INDEX );
            _aliases.Add( new Tuple<string, NavigationItem>( firstEntityAlias, firstEntityNavItem ) );
            // var result = from x0 in entities.Formulae
            return string.Format( "{0}var {1} = {2} {3} {4} {5}.{6}", Environment.NewLine, QUERY_PARAM_NAME, FROM, firstEntityAlias, IN, CONTEXT, firstEntityNavItem.EntitySetName );
        }

        private string Joins()
        {
            var result = string.Empty;
            var alias = STARTING_ALIAS;

            /// Outer loop is for each entity model navigator.  This guy represents the chain of related entities in the model between the entity that 
            /// is our source (like Student) and our target (like Institution).
            for ( int i = 0; i < _entityModelNavigators.Count; i++ )
            {
                var emn = _entityModelNavigators[i];
                var currentNavListItem = emn.Chain.First.Next;

                if ( i > 0 )
                {
                    IncrementAlias( ref alias );
                }
                var aliasIndex = STARTING_ALIAS_INDEX + 1;  // first alias number was used in FirstEntity()

                /// Iterate through the entities in the current relationship
                while ( currentNavListItem != null )
                {
                    var currentNavItem = currentNavListItem.Value;
                    var previousNavItem = currentNavListItem.Previous.Value;

                    /// In this method we always (even if there is only one item in _entityModelNavigators) start with the second nav item in the list.
                    /// The first nav item we process always joins from the first alias in the first list (Contract has ensured that each list begins
                    /// with the same entity type).
                    /// So if we're joining from student to Institution & Period (2 items in our main _entityModelNavigators list), student will be a1
                    /// and will relate both to Institution and Period using that alias.
                    var currentAlias = string.Format( "{0}{1}", alias, aliasIndex );
                    var previousAlias = ( aliasIndex == STARTING_ALIAS_INDEX + 1 ) ? _aliases[0].Item1 : string.Format( "{0}{1}", alias, aliasIndex - 1 );

                    _currentNavItemIsAssignmentTable = NavItemIsAssignmentTable( currentNavItem );
                    SetKeyColumns( currentNavItem, previousNavItem, true );

                    if ( !_currentNavItemIsAssignmentTable )
                    {
                        // Ex: join currentAlias in context.Fields on previousAlias.FormulaId equals currentAlias.FormulaId
                        result += string.Format( "{0}{1} {2} {3} {4}.{5} {6} {7}.{8} {9} {2}.{10}",
                                                Environment.NewLine, JOIN, currentAlias, IN, CONTEXT, currentNavItem.EntitySetName, ON, previousAlias, _previousEntityKeyColumn.Item1, EQUALS, _currentEntityKeyColumn.Item1 );
                        _aliases.Add( new Tuple<string, NavigationItem>( currentAlias, currentNavItem ) );
                    }

                    currentNavListItem = currentNavListItem.Next;
                    if ( AddCondition( emn, currentNavListItem, currentAlias ) )
                    {
                        break;
                    }
                    aliasIndex++;
                }
            }

            return result;
        }

        private string Conditions()
        {
            var result = string.Empty;
            _conditions.ForEach( c => result += c );
            return result;
        }

        private static string FinalSelect()
        {
            return string.Format( "{0}{1} {2}{3};{0}", Environment.NewLine, SELECT, STARTING_ALIAS, STARTING_ALIAS_INDEX );
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>Code from returning the result of the method to finishing up namespace</returns>
        private static string MethodPost()
        {
            return string.Format( "return {0};{1}{2}{1}{2}{1}{2}", QUERY_PARAM_NAME, Environment.NewLine, "}" );
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="currentAlias">like 'A'</param>
        /// <returns>'A' increments to 'B', 'Z' increments to 'a'</returns>
        /// <remarks>If you're passing in more than 54 related entities, I think you're pushing this too far...</remarks>
        private static void IncrementAlias( ref char currentAlias )
        {
            var charNum = (int)currentAlias;
            charNum = charNum == (int)'Z' ? (int)'a' : charNum + 1;
            currentAlias = (char)charNum;
        }

        /// <summary>
        /// If we've come to the penultimate entity in the current chain, filter on the foreign key of current entity
        /// (unless the penultimate entity is the primary in relationship to the last entity).
        /// </summary>
        private bool AddCondition( EntityModelNavigator currentEntityModelNavigator, LinkedListNode<NavigationItem> navItem, string currentAlias )
        {
            if ( ( navItem == null ) || ( navItem.Next == null ) )  // first check is for a 2 item chain (in that case, first item is penultimate).
            {
                /// If current is null, that means we either have a 2 item chain or the final table is a child of the penultimate table and we had to join in the last table.
                /// In the second case, _finalTableIsChild is true - see below.
                if ( navItem == null )
                {
                    if ( _finalTableIsChild )
                    {
                        /// No need to call SetKeyColumns; we just need to do this before adding the condition.
                        _currentEntityKeyColumn = new Tuple<string, bool>( currentEntityModelNavigator.Chain.Last.Value.PrimaryKeyColumn, false );
                    }
                }
                else
                {
                    var lastForwardNav = navItem.Previous.Value.CorrespondingForwardNav;
                    if ( ( lastForwardNav != null ) && NavItemIsPrimary( lastForwardNav ) )
                    {
                        _finalTableIsChild = true;
                        return false;  // will need to join in final table
                    }
                    SetKeyColumns( navItem.Value, navItem.Previous.Value, false );
                }
                AddConditionToList( currentEntityModelNavigator, navItem, currentAlias );
                return true;
            }
            else
            {
                return false;
            }
        }

        private void AddConditionToList( EntityModelNavigator currentEntityModelNavigator, LinkedListNode<NavigationItem> navItem, string currentAlias )
        {
            if ( currentEntityModelNavigator.ToEntityFilter is IList<int> )
            {
                AddListCondition( currentEntityModelNavigator, navItem, currentAlias );
            }
            else
            {
                AddScalarCondition( currentEntityModelNavigator, navItem, currentAlias );
            }
        }

        /// <summary>
        /// Very similar to below method, but too much difference to easily make into one method
        /// </summary>
        private void AddScalarCondition( EntityModelNavigator currentEntityModelNavigator, LinkedListNode<NavigationItem> navItem, string currentAlias )
        {
            string condition;
            if ( _currentNavItemIsAssignmentTable && !EqualsPreviousNavItem( currentEntityModelNavigator, navItem ) )
            {
                /// Ex: previousAlias.Things.Any( x => x.KeyColumn == 1 )
                /// 0: previousAlias 1:_previousEntityKeyColumn 2:_currentEntityKeyColumn 3:currentEntityModelNavigator.ToEntityFilter
                var previousAlias = GetPreviousAlias( currentEntityModelNavigator, navItem );
                var currentKeyReference = GetCurrentKeyReference();
                condition = string.Format( "({0}.{1}.Any( x => {2} == {3} ))", previousAlias, _previousEntityKeyColumn.Item1, currentKeyReference, currentEntityModelNavigator.ToEntityFilter );
            }
            else
            {
                /// Ex: (_currentEntityKeyColumn.Id == 1234)
                var aliasReference = GetCurrentAliasReference( currentAlias );
                condition = string.Format( "({0} == {1})", aliasReference, currentEntityModelNavigator.ToEntityFilter );
            }
            AddCondition( condition );

        }

        /// <summary>
        /// Very similar to above method, but too much difference to easily make into one method
        /// </summary>
        private void AddListCondition( EntityModelNavigator currentEntityModelNavigator, LinkedListNode<NavigationItem> navItem, string currentAlias )
        {
            Contract.Invariant( currentEntityModelNavigator.ToEntityFilter is IList<int> );
            string condition;
            var listParamName = string.Format( "{0}{1}", LIST, ++_listCount );

            var previousAlias = GetPreviousAlias( currentEntityModelNavigator, navItem );
            if ( _currentNavItemIsAssignmentTable && !EqualsPreviousNavItem( currentEntityModelNavigator, navItem ) )
            {
                var currentKeyReference = GetCurrentKeyReference();
                /// Example: previousAlias.Things.Any( x => listParamName.Contains(x.KeyColumn))
                /// 0: previousAlias 1:_previousEntityKeyColumn 2:listParamName 3:currentKeyColumn
                condition = string.Format( "( {0}.{1}.Any( x => {2}.Contains({3})) )", previousAlias, _previousEntityKeyColumn.Item1, listParamName, currentKeyReference );
            }
            else
            {
                /// Ex: ( list1.Contains(currentAlias.Id) )
                var alias = _currentNavItemIsAssignmentTable ? previousAlias : currentAlias;
                var aliasReference = GetCurrentAliasReference( alias );
                condition = string.Format( "( {0}.Contains({1}) )", listParamName, aliasReference );
            }
            AddCondition( condition );
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="alias"></param>
        /// <returns>Ex: alias.Id or alias.Id ?? 0</returns>
        private string GetCurrentAliasReference( string alias )
        {
            var result = string.Format( "{0}.{1}", alias, _currentEntityKeyColumn.Item1 );
            if ( _currentEntityKeyColumn.Item2 )
            {
                result = string.Format( NULLABLE, result );  // Ex: ( list1.Contains( (currentAlias.Id ?? 0) ) )
            }
            return result;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>Ex: x.Id or (x.Id ?? 0)</returns>
        private string GetCurrentKeyReference()
        {
            var result = "x." + _currentEntityKeyColumn.Item1;
            if ( _currentEntityKeyColumn.Item2 )
            {
                result = string.Format( NULLABLE, result );
            }
            return result;
        }

        private static bool EqualsPreviousNavItem( EntityModelNavigator currentEntityModelNavigator, LinkedListNode<NavigationItem> navItem )
        {
            if ( navItem == null )
            {
                var lastNavItem = currentEntityModelNavigator.Chain.Last;
                return lastNavItem.Value == lastNavItem.Previous.Value;
            }
            else
            {
                return navItem.Value == navItem.Previous.Value;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <remarks>If this returns an empty string, we're just going to let our parent fail to compile and error that way.</remarks>
        private string GetPreviousAlias( EntityModelNavigator currentEntityModelNavigator, LinkedListNode<NavigationItem> navItem )
        {
            /// _aliases will always have at least one (the first) alias by this point.

            /// establish current node
            var currentNode = navItem ?? currentEntityModelNavigator.Chain.Last;
            var currentToEntity = currentNode.Value.ToEntity;

            /// Now traverse the alias list backwards until you find a node referencing that same entity set.
            Tuple<string, NavigationItem> aliasNode = null;
            for ( int i = _aliases.Count - 1; i > -1; i-- )
            {
                if ( _aliases[i].Item2.FromEntity == currentToEntity )
                {
                    aliasNode = _aliases[i];
                    break;
                }
                if ( i == 0 )
                {
                    aliasNode = _aliases[0];
                }
            }
            return aliasNode.Item1;  // will be assigned (list is never empty at this point).
        }

        private void AddCondition( string condition )
        {
            if ( _conditions.Count == 0 )
            {
                _conditions.Add( string.Format( "{0}{1} {2}", Environment.NewLine, WHERE, condition ) );
            }
            else
            {
                _conditions.Add( " && " + condition );
            }
        }

        /// <summary>
        /// Set which columns to use in relating the 2 entities
        /// </summary>
        private void SetKeyColumns( NavigationItem navItem, NavigationItem previousNavItem, bool forJoin )
        {
            /// See EntityModelNavigator.SetChainForEntitySearchingForItself
            if ( navItem.FromEntity == previousNavItem.FromEntity )
            {
                _previousEntityKeyColumn = new Tuple<string, bool>( navItem.PrimaryKeyColumn, false );
                _currentEntityKeyColumn = _previousEntityKeyColumn;
            }
            else
            {
                if ( _currentNavItemIsAssignmentTable )
                {
                    _previousEntityKeyColumn = _previousEntityKeyColumn.Item1 == previousNavItem.EntitySetName ? _previousEntityKeyColumn : new Tuple<string, bool>( navItem.EntitySetName, false );
                    _currentEntityKeyColumn = ( navItem == previousNavItem ) ? new Tuple<string, bool>( navItem.PrimaryKeyColumn, false ) : ( string.IsNullOrEmpty( navItem.ForeignKeyColumn ) ? new Tuple<string, bool>( navItem.PrimaryKeyColumn, false ) : new Tuple<string, bool>( navItem.ForeignKeyColumn, navItem.ForeignKeyNullable ) );
                }
                else if ( previousNavItem.ToEntity != navItem.FromEntity )
                {
                    SetKeyColumns_NonStandardPair( previousNavItem, navItem, forJoin );
                }
                else if ( NavItemIsPrimary( previousNavItem ) )
                {
                    _previousEntityKeyColumn = new Tuple<string, bool>( previousNavItem.PrimaryKeyColumn, false );
                    _currentEntityKeyColumn = new Tuple<string, bool>( navItem.ForeignKeyColumn, navItem.ForeignKeyNullable );
                }
                else
                {
                    _previousEntityKeyColumn = new Tuple<string, bool>( previousNavItem.ForeignKeyColumn, previousNavItem.ForeignKeyNullable );
                    _currentEntityKeyColumn = new Tuple<string, bool>( navItem.PrimaryKeyColumn, false );
                }
            }

        }

        /// <summary>
        /// A "standard" pair of nav items between entities would mean the previous nav item's "to" points to the current's "from"
        /// </summary>
        /// <param name="previousNavItem"></param>
        /// <param name="navItem"></param>
        /// <param name="forJoin">This method is called when adding a condition and when forming a join.</param>
        private void SetKeyColumns_NonStandardPair( NavigationItem previousNavItem, NavigationItem navItem, bool forJoin )
        {
            if ( previousNavItem.CorrespondingForwardNav == null )
            {
                throw new Exception( "Expected navigation between entities missing while building object query" );
            }
            if ( NavItemIsPrimary( navItem ) && forJoin )
            {
                _previousEntityKeyColumn = new Tuple<string, bool>( previousNavItem.CorrespondingForwardNav.ForeignKeyColumn, previousNavItem.CorrespondingForwardNav.ForeignKeyNullable );
                _currentEntityKeyColumn = new Tuple<string, bool>( previousNavItem.PrimaryKeyColumn, true );
            }
            else
            {
                _previousEntityKeyColumn = new Tuple<string, bool>( previousNavItem.PrimaryKeyColumn, false );
                _currentEntityKeyColumn = new Tuple<string, bool>( previousNavItem.CorrespondingForwardNav.ForeignKeyColumn, previousNavItem.CorrespondingForwardNav.ForeignKeyNullable );
            }
        }

        /// <summary>
        /// This item (entity set) would represent a many-to-many table that doesn't actually appear in the model.
        /// </summary>
        private static bool NavItemIsAssignmentTable( NavigationItem ni )
        {
            return ni.FromMultiplicity == RelationshipMultiplicity.Many && ni.ToMultiplicity == RelationshipMultiplicity.Many;
        }

        /// <summary>
        /// Primary or parent
        /// </summary>
        private static bool NavItemIsPrimary( NavigationItem ni )
        {
            return ni.FromMultiplicity == RelationshipMultiplicity.One || ni.FromMultiplicity == RelationshipMultiplicity.ZeroOrOne;
        }

    }

}
